index.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362
  1. import Link from "next/link";
  2. import Head from "next/head";
  3. import { useMemo } from "react";
  4. import { useRouter } from "next/router";
  5. import TabUnstyled from "@mui/base/TabUnstyled";
  6. import TabsUnstyled from "@mui/base/TabsUnstyled";
  7. import TabsListUnstyled from "@mui/base/TabsListUnstyled";
  8. import TabPanelUnstyled from "@mui/base/TabPanelUnstyled";
  9. import type { GetServerSideProps, NextPage } from "next";
  10. import { get } from "libs/http";
  11. import MyError from "pages/_error";
  12. import useGet from "libs/hooks/useGet";
  13. import errorProps from "libs/errorProps";
  14. import useStore from "libs/hooks/useStore";
  15. import NovelCover from "components/NovelCover";
  16. import styles from "styles/novel-info.module.scss";
  17. import { SeoHead, SeoHeadConfig } from "components/SeoHead";
  18. interface NovelPageProps {
  19. detail?: Detail;
  20. chapters?: ChapterListData;
  21. statusCode?: number;
  22. }
  23. const Novel: NextPage<NovelPageProps> = (props) => {
  24. const { statusCode } = props;
  25. const { siteConfig } = useStore();
  26. const { query } = useRouter();
  27. const { data: { data: detail } = { data: null } } = useGet<Detail>(
  28. `/api/novel/${query.slug}`
  29. );
  30. const { data: { data: chapters } = { data: null } } = useGet<ChapterListData>(
  31. `/api/novel/${query.slug}/chapters`
  32. );
  33. const seoConfig: SeoHeadConfig = useMemo(() => {
  34. const keys = detail?.genres.map((item) => item.name).join(", ");
  35. return {
  36. title: `${detail?.name} - ${siteConfig.siteName}`,
  37. description: `${detail?.name} is ${keys} web novel. Read ${detail?.name} novel written by the author ${detail?.author} on ${siteConfig.siteName} for Free.`,
  38. keywords: `${[
  39. detail?.name,
  40. detail?.author,
  41. detail?.other_name,
  42. keys,
  43. siteConfig.keywords,
  44. siteConfig.siteName,
  45. ]
  46. .filter((item) => item)
  47. .join(", ")}`,
  48. url: `https://${siteConfig.host}/novel/${query.slug}`,
  49. siteName: siteConfig.siteName,
  50. img: detail?.img,
  51. jsonLd: JSON.stringify([
  52. {
  53. "@context": "https://schema.org",
  54. "@type": "Book",
  55. mainEntityOfPage: `https://${siteConfig.host}/novel/${query.slug}`,
  56. headline: detail?.name,
  57. name: detail?.name,
  58. genre: detail?.genres[0].name,
  59. image: {
  60. "@type": "ImageObject",
  61. url: detail?.img,
  62. },
  63. bookFormat: "https://schema.org/EBook",
  64. datePublished: detail?.create_time,
  65. dateModified: detail?.update_time,
  66. author: {
  67. "@type": "Person",
  68. name: detail?.author,
  69. },
  70. copyrightHolder: detail?.author,
  71. publisher: {
  72. "@type": "Organization",
  73. name: siteConfig.siteName,
  74. logo: {
  75. "@type": "ImageObject",
  76. url: `https://${siteConfig.host}/favicon-32x32.png`,
  77. },
  78. },
  79. description: detail?.desc,
  80. // aggregateRating: {
  81. // "@type": "AggregateRating",
  82. // bestRating: "5.0",
  83. // ratingValue: "4.62",
  84. // ratingCount: "159",
  85. // },
  86. potentialAction: {
  87. "@type": "ReadAction",
  88. target: {
  89. "@type": "EntryPoint",
  90. urlTemplate: `https://${siteConfig.host}/novel/${chapters?.chapters[0].uri}`,
  91. },
  92. },
  93. },
  94. {
  95. "@context": "https://schema.org",
  96. "@type": "BreadcrumbList",
  97. itemListElement: [
  98. {
  99. "@type": "ListItem",
  100. position: 1,
  101. name: "Home",
  102. item: `https://${siteConfig.host}`,
  103. },
  104. {
  105. "@type": "ListItem",
  106. position: 2,
  107. name: detail?.genres[0].name,
  108. item: `https://${siteConfig.host}/novels/${detail?.genres[0].uri}`,
  109. },
  110. {
  111. "@type": "ListItem",
  112. position: 3,
  113. name: `${detail?.name || ""}`,
  114. item: `https://${siteConfig.host}/novel/${query.slug}`,
  115. },
  116. ],
  117. },
  118. ...siteConfig.jsonLd,
  119. ]),
  120. };
  121. }, [
  122. chapters?.chapters,
  123. detail?.author,
  124. detail?.create_time,
  125. detail?.desc,
  126. detail?.genres,
  127. detail?.img,
  128. detail?.name,
  129. detail?.other_name,
  130. detail?.update_time,
  131. query.slug,
  132. siteConfig.host,
  133. siteConfig.jsonLd,
  134. siteConfig.keywords,
  135. siteConfig.siteName,
  136. ]);
  137. const chapterLists = useMemo(() => {
  138. if (!chapters || !chapters.chapters.length) return [];
  139. const len = Math.ceil(chapters.chapters.length / 100);
  140. const list = [];
  141. for (let i = 0; i < len; i++) {
  142. list.push({
  143. title: `${i * 100 + 1}-${(i + 1) * 100}`,
  144. list: chapters.chapters.slice(
  145. i * 100,
  146. Math.min(chapters.chapters.length, (i + 1) * 100)
  147. ),
  148. });
  149. }
  150. return list;
  151. }, [chapters]);
  152. if (statusCode) {
  153. return <MyError statusCode={statusCode} />;
  154. }
  155. if (!detail || !chapters) {
  156. return null;
  157. }
  158. return (
  159. <main>
  160. <SeoHead seoConfig={seoConfig} />
  161. <div
  162. className={styles["novel-wrap"]}
  163. style={{
  164. backgroundImage: `url(${detail?.img})`,
  165. }}
  166. >
  167. <div className={styles["novel-container"]}>
  168. <div className="text-sm py-4 breadcrumbs">
  169. <ul>
  170. <li>
  171. <Link href="/" title="Home">
  172. <svg className="w-5 h-5 mr-1 -mt-1">
  173. <use xlinkHref="/icons.svg#home"></use>
  174. </svg>
  175. Home
  176. </Link>
  177. </li>
  178. {detail.genres && detail.genres.length > 0 ? (
  179. <li>
  180. <Link
  181. title={detail.genres[0].name}
  182. href={`/novels/${detail.genres[0].uri}`}
  183. >
  184. {detail.genres[0].name}
  185. </Link>
  186. </li>
  187. ) : null}
  188. <li>{detail.name}</li>
  189. </ul>
  190. </div>
  191. <div className={styles["novel-info"]}>
  192. <NovelCover
  193. className={styles["novel-info-cover"]}
  194. component="div"
  195. alt={detail.name}
  196. src={detail.img}
  197. />
  198. <div className={styles["nove-info-body"]}>
  199. <h1>
  200. {detail.name}
  201. <small>Completed</small>
  202. </h1>
  203. <h2>
  204. {detail.genres && detail.genres.length > 0 ? (
  205. <Link
  206. title={detail.genres[0].name}
  207. href={`/novels/${detail.genres[0].uri}`}
  208. >
  209. <svg>
  210. <use xlinkHref="/icons.svg#paper"></use>
  211. </svg>
  212. <span>{detail.genres[0].name}</span>
  213. </Link>
  214. ) : null}
  215. <strong>
  216. <svg>
  217. <use xlinkHref="/icons.svg#chapter"></use>
  218. </svg>
  219. <span>{chapters.chapters.length} Chapters</span>
  220. </strong>
  221. {/* <strong>
  222. <svg>
  223. <use xlinkHref="/icons.svg#eye"></use>
  224. </svg>
  225. <span>0 Views</span>
  226. </strong> */}
  227. </h2>
  228. <div className={styles["btns"]}>
  229. <Link
  230. href={`/novel/${chapters.chapters[0].uri}`}
  231. className={styles["button"]}
  232. >
  233. Start Reading
  234. </Link>
  235. </div>
  236. </div>
  237. </div>
  238. </div>
  239. </div>
  240. <TabsUnstyled defaultValue={0} className="container bg-paper py-3">
  241. <TabsListUnstyled className="tabs">
  242. <TabUnstyled value={0} className="tab">
  243. About
  244. </TabUnstyled>
  245. <TabUnstyled value={1} className="tab">
  246. Chapters
  247. </TabUnstyled>
  248. </TabsListUnstyled>
  249. <TabPanelUnstyled value={0}>
  250. <h2 className="sub-title">Tags</h2>
  251. <div className="tags">
  252. {detail.genres.map((item) => (
  253. <Link
  254. title={item.name}
  255. href={`/novels/${item.uri}`}
  256. className="tag"
  257. key={item.uri}
  258. >
  259. {item.name}
  260. </Link>
  261. ))}
  262. </div>
  263. <h2 className="sub-title">Synopsis</h2>
  264. <div
  265. className={styles["novel-text"]}
  266. dangerouslySetInnerHTML={{ __html: detail.desc }}
  267. />
  268. </TabPanelUnstyled>
  269. <TabPanelUnstyled value={1}>
  270. <h3 className="sub-title">{detail.name} Chapters</h3>
  271. {/* {isServer ? (
  272. <ol className={styles["chapter-list"]}>
  273. {chapters.chapters.map((item) => (
  274. <li key={item.id}>
  275. <Link href={`/novel/${item.uri}`} title={item.name}>
  276. <i>1</i>
  277. <strong>{item.name}</strong>
  278. <small>1yr</small>
  279. </Link>
  280. </li>
  281. ))}
  282. </ol>
  283. ) : ( */}
  284. <TabsUnstyled defaultValue={0} className="container bg-paper py-3">
  285. <TabsListUnstyled className="tabs">
  286. {chapterLists.map((chapter, idx) => (
  287. <TabUnstyled value={idx} className="tab" key={chapter.title}>
  288. {chapter.title}
  289. </TabUnstyled>
  290. ))}
  291. </TabsListUnstyled>
  292. {chapterLists.map((chapter, idx) => (
  293. <TabPanelUnstyled
  294. value={idx}
  295. component="ol"
  296. key={chapter.title}
  297. className={styles["chapter-list"]}
  298. >
  299. {chapter.list.map((item) => (
  300. <li key={item.id}>
  301. <Link href={`/novel/${item.uri}`} title={item.name}>
  302. <i>1</i>
  303. <strong>{item.name}</strong>
  304. <small>1yr</small>
  305. </Link>
  306. </li>
  307. ))}
  308. </TabPanelUnstyled>
  309. ))}
  310. </TabsUnstyled>
  311. {/* )} */}
  312. </TabPanelUnstyled>
  313. </TabsUnstyled>
  314. </main>
  315. );
  316. };
  317. export const getServerSideProps: GetServerSideProps<
  318. Docs,
  319. { slug: string }
  320. > = async (context) => {
  321. if (!context.params) {
  322. return errorProps(context);
  323. }
  324. try {
  325. const { slug } = context.params;
  326. const [detail, chapters] = await Promise.all([
  327. get<Detail>(`/api/novel/${slug}`),
  328. get<ChapterListData>(`/api/novel/${slug}/chapters`),
  329. ]);
  330. if (!detail || !detail.data.id) {
  331. return errorProps(context);
  332. }
  333. return {
  334. props: {
  335. fallback: {
  336. [`/api/novel/${slug}`]: detail,
  337. [`/api/novel/${slug}/chapters`]: chapters,
  338. },
  339. },
  340. };
  341. } catch (e) {
  342. return errorProps(context, 500);
  343. }
  344. };
  345. export default Novel;